FlutterでPluginプロジェクトを作って実装してみた
大阪オフィスの山田です。あいぽんのショートカットアプリが楽しくて時間がみるみるうちに無くなります。FlutterでPluginを作る流れをやってみたのでご紹介します。(後述する理由により、公開はしてません)今回、サンプルとして、タイトルとメッセージを渡してネイティブのダイアログを表示するプラグインを作ってみます。こちらが公式のページです
実行画面
iOS
Android
開発環境
flutter doctor
[✓] Flutter (Channel master, v0.7.3-pre.24, on Mac OS X 10.13.6 17G65, locale ja-JP) [✓] Android toolchain - develop for Android devices (Android SDK 28.0.1) [✓] iOS toolchain - develop for iOS devices (Xcode 9.4) [✓] Android Studio (version 3.1) ✗ Flutter plugin not installed; this adds Flutter specific functionality. ✗ Dart plugin not installed; this adds Dart specific functionality. [✓] VS Code (version 1.27.2) [✓] Connected devices (1 available)
Packageについて
Packageは、以下の2種類が存在します。
- Dart package: Flutterフレームワークのみを使ったDartのみのPackage
- Plugin package: ネイティブAPIを使ったPackage(今回はこちら)
Packageは最低限、以下のファイルを含みます。
pubspec.yaml
: 名前やバージョンといったpackageのmetadataを記載します。lib
: packageのコードを含みます。最低限、<package-name>.dart
ファイルを含みます。
プロジェクトを作る
プロジェクトを作成する時に、--template=plugin
を付与することで、plugin用のプロジェクトが作られます。
flutter create --org com.yamadaryo --template=plugin -i swift -a kotlin hogehoge
他のパラメータについての解説を載せておきます。
--org
: organaizationを指定します。通常、Reverse domain nameで指定します-i swift -a kotlin
: デフォルトでプロジェクトのネイティブコードは、Objective-CとJavaで生成されますが、SwiftとKotlinを使いたい場合は、このオプションで指定します。
プロジェクトの設定
iOSの設定
Before editing the iOS platform code in Xcode, first make sure that the code has been built at least once
と公式ページに記載がありますので、Xcodeで開く前に一度、ビルドをします。こちらのコマンドでExampleプロジェクトのビルドを走らせます。
cd ./example flutter build ios --no-codesign
ビルドした際にエラーが出ましたが、こちらのIssueで解決策が提示されていました。ios/Flutter/Debug.xcconfig
にFLUTTER_BUILD_MODE=debug
の記述を追加します。
Androidの設定
AndroidもiOSと同様に
Before editing the Android platform code in Android Studio, first make sure that the code has been built at least once
と公式ページに記載がありますので、Android Studioで開く前に一度、ビルドをします。こちらのコマンドでExampleプロジェクトのビルドを走らせます。
cd ./example flutter build apk
実装
以下の実装が必要になります。 - Flutter側の実装 - プラグインの実行モジュール - Example - iOS(MethodChannel) - Android(MethodChannel)
今回、ダイアログのタイトル文字列と、メッセージ文字列を渡しています。
Flutter側の実装
MethodChannelでの呼び出し
lib/<プラグイン名>.dart
に実装します。
import 'dart:async'; import 'package:flutter/services.dart'; class PlatformOriginDialog { static const MethodChannel _channel = const MethodChannel('platform_origin_dialog'); static Future<String> showDialog(String title, String message) async { final String result = await _channel.invokeMethod( 'show_dialog', <String, dynamic>{ 'title': title, 'message': message, }); return result; } }
Example
Pluginを動かすExampleもプロジェクトに含まれますので、example/lib/main.dart
にExampleを実装します。
import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter/services.dart'; import 'package:platform_origin_dialog/platform_origin_dialog.dart'; void main() => runApp(new MyApp()); class MyApp extends StatefulWidget { @override _MyAppState createState() => new _MyAppState(); } class _MyAppState extends State<MyApp> { String _dialogResult = 'Unknown'; @override void initState() { super.initState(); } Future<void> initPlatformState() async { String dialogResult; try { dialogResult = await PlatformOriginDialog.showDialog("確認", "保存しますか?"); } on PlatformException { dialogResult = 'Failed to show Dialog.'; } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. if (!mounted) return; setState(() { _dialogResult = dialogResult; }); } @override Widget build(BuildContext context) { return new MaterialApp( home: new Scaffold( appBar: new AppBar( title: const Text('Plugin example app'), ), body: Center(child: Column(children: <Widget>[ new Text('Dialog result: $_dialogResult\n'), MaterialButton( child: Text("Button"), onPressed: () { initPlatformState(); },) ], mainAxisAlignment: MainAxisAlignment.spaceEvenly, )) ), ); } }
iOSの実装
iOS/Classes/SwiftPlatform<プラグイン名>.swift
に実装をしていきます。
public class SwiftPlatformOriginDialogPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "platform_origin_dialog", binaryMessenger: registrar.messenger()) let instance = SwiftPlatformOriginDialogPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { if call.method == "show_dialog" { let arguments = call.arguments as! [String: Any] guard let title = arguments["title"] as? String, let message = arguments["message"] as? String else { result(FlutterError.init(code: "ArgumentError", message: "Required argument does not exist.", details: nil)); return } let alert: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert) let controller : FlutterViewController = UIApplication.shared.keyWindow?.rootViewController as! FlutterViewController; let defaultAction: UIAlertAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler:{ (action: UIAlertAction!) -> Void in result("Tap OK") }) let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler:{ (action: UIAlertAction!) -> Void in result("Tap Cancel") }) alert.addAction(cancelAction) alert.addAction(defaultAction) controller.present(alert, animated: true, completion: nil) } } }
Androidの実装
android/src/main/kotlin/<ドメイン名>/<プラグイン名>Plugin.kt
に実装をしていきます。
class PlatformOriginDialogPlugin(registrar: PluginRegistry.Registrar): MethodCallHandler { var registrar: Registrar = registrar companion object { @JvmStatic fun registerWith(registrar: Registrar): Unit { val channel = MethodChannel(registrar.messenger(), "platform_origin_dialog") channel.setMethodCallHandler(PlatformOriginDialogPlugin(registrar)) } } override fun onMethodCall(call: MethodCall, result: Result): Unit { if (call.method.equals("show_dialog")) { val context = registrar.view().context val title = call.argument<String>("title") val message = call.argument<String>("message") AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog_Alert).apply { setTitle(title) setMessage(message) setPositiveButton("OK", DialogInterface.OnClickListener { _, _ -> result.success("Tap OK") }) setNegativeButton("Cancel", DialogInterface.OnClickListener { _, _ -> result.success("Tap Cancel") }) show() } } } }
公開する
flutter packages pub publish --dry-run
コマンドを実行し、不備がないかチェックします。問題がなければ、flutter packages pub publish
で公開します。
一度公開すると削除はできないため、今回作ったテスト用Pluginは公開していません。詳細はこちら
最後に
久しぶりの投稿となってしまいました。アップデートも来ているようなので新しい部分も触っていこうと思います。